/*
 * Created on 17-Jan-2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.networkmanager.impl;

import java.io.IOException;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.util.Map;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.logging.LogAlert;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AddressUtils;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.LightHashMap;
import org.gudy.azureus2.core3.util.RandomUtils;
import org.gudy.azureus2.core3.util.SHA1Hasher;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.networkmanager.NetworkManager;
import com.aelitis.azureus.core.util.bloom.BloomFilter;
import com.aelitis.azureus.core.util.bloom.BloomFilterFactory;

public class ProtocolDecoderPHE extends ProtocolDecoder {
    private static final LogIDs LOGID = LogIDs.NWMAN;

    private static final byte CRYPTO_PLAIN = 0x01;
    private static final byte CRYPTO_RC4 = 0x02;
    private static final byte CRYPTO_XOR = 0x04;
    private static final byte CRYPTO_AES = 0x08;

    // private static final String DH_P =
    // "92d862b3a95bff4e6cbdce3a266ff4b46e6e1ecad76c0a877d92a3dae4999e6414efde56fc14d1cca6d5408a8ef9ea248389168876b6e8f4503845dfe373549f";
    // private static final String DH_G =
    // "4383b53ee650fd73e41e8c9e8527997ab8cb41e1cbd73ac7685493e1e5d091e3e3789dea03ab9d5b2c368faa617bb30e427cbaeb23c268edb38eb8c747756080";
    // private static final String DH_P =
    // "f3f90c790c63b119f9c1be43fdb12dc6ed6f26325999c01ba6ed373e75d6b2dee8d1c0475652a987c8df57b23d395bdb142be316d780b9361f85629535030873";

    private static final String DH_P =
            "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A63A36210000000000090563";
    private static final String DH_G = "02";
    private static final int DH_L = 160;

    private static final int DH_SIZE_BYTES = DH_P.length() / 2;

    public static final int MIN_INCOMING_INITIAL_PACKET_SIZE = DH_SIZE_BYTES;

    private static final BigInteger DH_P_BI = new BigInteger(DH_P, 16);
    private static final BigInteger DH_G_BI = new BigInteger(DH_G, 16);

    private static KeyPairGenerator dh_key_generator;
    private static long last_dh_incoming_key_generate;

    private static final int BLOOM_RECREATE = 30 * 1000;
    private static final int BLOOM_INCREASE = 1000;
    private static BloomFilter generate_bloom = BloomFilterFactory.createAddRemove4Bit(BLOOM_INCREASE);
    private static long generate_bloom_create_time = SystemTime.getCurrentTime();

    private static boolean crypto_setup_done;
    private static boolean crypto_ok;
    // private static boolean aes_ok;

    /*
     * private static final String AES_STREAM_ALG = "AES"; private static final String AES_STREAM_CIPHER = "AES/CFB8/NoPadding"; private static final
     * int AES_STREAM_KEY_SIZE = 128; private static final int AES_STREAM_KEY_SIZE_BYTES = AES_STREAM_KEY_SIZE/8;
     */

    // private static final byte[] AES_STREAM_IV =
    // { (byte)0x15, (byte)0xE0, (byte)0x6B, (byte)0x7E, (byte)0x98, (byte)0x59, (byte)0xE4, (byte)0xA7,
    // (byte)0x34, (byte)0x66, (byte)0xAD, (byte)0x48, (byte)0x35, (byte)0xE2, (byte)0xD0, (byte)0x24 };

    private static final String RC4_STREAM_ALG = "RC4";
    private static final String RC4_STREAM_CIPHER = "RC4";
    private static final int RC4_STREAM_KEY_SIZE = 128;
    private static final int RC4_STREAM_KEY_SIZE_BYTES = RC4_STREAM_KEY_SIZE / 8;

    private static final int PADDING_MAX = 512;

    private static final int PADDING_MAX_NORMAL = PADDING_MAX;
    private static final int PADDING_MAX_LIMITED = 128;

    public static int getMaxIncomingInitialPacketSize(boolean min_overheads) {
        return (MIN_INCOMING_INITIAL_PACKET_SIZE + (min_overheads ? PADDING_MAX_LIMITED : PADDING_MAX_NORMAL) / 2);
    }

    private static Random random = RandomUtils.SECURE_RANDOM;

    private static Map global_shared_secrets = new LightHashMap();

    private static boolean cryptoSetup() {
        synchronized (global_shared_secrets) {

            if (crypto_setup_done) {

                return (crypto_ok);
            }

            crypto_setup_done = true;

            try {
                DHParameterSpec dh_param_spec = new DHParameterSpec(DH_P_BI, DH_G_BI, DH_L);

                dh_key_generator = KeyPairGenerator.getInstance("DH");

                dh_key_generator.initialize(dh_param_spec);

                dh_key_generator.generateKeyPair();

                byte[] rc4_test_secret = new byte[RC4_STREAM_KEY_SIZE_BYTES];

                SecretKeySpec rc4_test_secret_key_spec = new SecretKeySpec(rc4_test_secret, 0, RC4_STREAM_KEY_SIZE_BYTES, RC4_STREAM_ALG);

                TransportCipher rc4_cipher = new TransportCipher(RC4_STREAM_CIPHER, Cipher.ENCRYPT_MODE, rc4_test_secret_key_spec);

                rc4_cipher = new TransportCipher(RC4_STREAM_CIPHER, Cipher.DECRYPT_MODE, rc4_test_secret_key_spec);

                /*
                 * try{ byte[] aes_test_secret = new byte[AES_STREAM_KEY_SIZE_BYTES]; SecretKeySpec aes_test_secret_key_spec = new
                 * SecretKeySpec(aes_test_secret, 0, AES_STREAM_KEY_SIZE_BYTES, AES_STREAM_ALG ); AlgorithmParameterSpec spec = new IvParameterSpec(
                 * aes_test_secret ); TCPTransportCipher aes_cipher = new TCPTransportCipher( AES_STREAM_CIPHER, Cipher.ENCRYPT_MODE,
                 * aes_test_secret_key_spec, spec ); aes_cipher = new TCPTransportCipher( AES_STREAM_CIPHER, Cipher.DECRYPT_MODE,
                 * aes_test_secret_key_spec, spec ); aes_ok = true; }catch( Throwable e ){ Logger.log( new LogEvent(LOGID, "AES Unavailable", e )); }
                 */

                crypto_ok = true;

                if (Logger.isEnabled()) {

                    Logger.log(new LogEvent(LOGID, "PHE crypto initialised"));
                }
            } catch (NoClassDefFoundError e) {

                // running without PHE classes, not such a severe error

                Logger.log(new LogEvent(LOGID, "PHE crypto disabled as classes unavailable"));

                crypto_ok = false;

            } catch (Throwable e) {

                Logger.log(new LogEvent(LOGID, "PHE crypto initialisation failed", e));

                crypto_ok = false;
            }

            return (crypto_ok);
        }
    }

    public static boolean isCryptoOK() {
        return (cryptoSetup());
    }

    public static void addSecretsSupport(byte[][] secrets) {
        for (int i = 0; i < secrets.length; i++) {

            SHA1Hasher hasher = new SHA1Hasher();

            hasher.update(REQ2_IV);
            hasher.update(secrets[i]);

            byte[] encoded = hasher.getDigest();

            synchronized (global_shared_secrets) {

                global_shared_secrets.put(new HashWrapper(encoded), secrets[i]);
            }
        }
    }

    public static void removeSecretsSupport(byte[][] secrets) {
        for (int i = 0; i < secrets.length; i++) {

            SHA1Hasher hasher = new SHA1Hasher();

            hasher.update(REQ2_IV);
            hasher.update(secrets[i]);

            byte[] encoded = hasher.getDigest();

            synchronized (global_shared_secrets) {

                global_shared_secrets.remove(new HashWrapper(encoded));
            }
        }
    }

    // private static final byte SUPPORTED_PROTOCOLS = (byte)((aes_ok?CRYPTO_AES:0) | CRYPTO_RC4 | CRYPTO_XOR | CRYPTO_PLAIN );
    private static final byte SUPPORTED_PROTOCOLS = (byte) (CRYPTO_RC4 | CRYPTO_PLAIN);

    private static byte MIN_CRYPTO;

    static {
        COConfigurationManager.addAndFireParameterListeners(new String[] { "network.transport.encrypted.min_level" }, new ParameterListener() {
            public void parameterChanged(String ignore) {
                if (NetworkManager.REQUIRE_CRYPTO_HANDSHAKE && !isCryptoOK()) {
                    Logger.log(new LogAlert(true, LogAlert.AT_ERROR, "Connection encryption unavailable, please update your Java version"));
                }

                String min = COConfigurationManager.getStringParameter("network.transport.encrypted.min_level");

                if (min.equals("XOR")) {

                    MIN_CRYPTO = CRYPTO_XOR | CRYPTO_RC4 | CRYPTO_AES;

                } else if (min.equals("RC4")) {

                    MIN_CRYPTO = CRYPTO_RC4 | CRYPTO_AES;

                } else if (min.equals("AES")) {

                    MIN_CRYPTO = CRYPTO_AES;

                } else {

                    MIN_CRYPTO = CRYPTO_PLAIN | CRYPTO_XOR | CRYPTO_RC4 | CRYPTO_AES;
                }

                MIN_CRYPTO = (byte) (MIN_CRYPTO & SUPPORTED_PROTOCOLS);
            }
        });
    }

    private static final int PS_OUTBOUND_1 = 0;
    private static final int PS_OUTBOUND_2 = 1;
    private static final int PS_OUTBOUND_3 = 2;
    private static final int PS_OUTBOUND_4 = 3;

    private static final int PS_INBOUND_1 = 10;
    private static final int PS_INBOUND_2 = 11;
    private static final int PS_INBOUND_3 = 12;
    private static final int PS_INBOUND_4 = 13;

    public static final byte[] KEYA_IV = "keyA".getBytes();
    public static final byte[] KEYB_IV = "keyB".getBytes();
    public static final byte[] REQ1_IV = "req1".getBytes();
    public static final byte[] REQ2_IV = "req2".getBytes();
    public static final byte[] REQ3_IV = "req3".getBytes();
    public static final byte[] VC = { 0, 0, 0, 0, 0, 0, 0, 0 };

    private TransportHelper transport;
    private ByteBuffer write_buffer;
    private ByteBuffer read_buffer;

    private ProtocolDecoderAdapter adapter;

    private KeyAgreement key_agreement;
    private byte[] dh_public_key_bytes;

    private byte[] shared_secret;
    private byte[] secret_bytes;

    private ByteBuffer initial_data_out;
    private ByteBuffer initial_data_in;

    private TransportCipher write_cipher;
    private TransportCipher read_cipher;

    private byte[] padding_skip_marker;

    private byte my_supported_protocols;
    private byte selected_protocol;

    private boolean outbound;

    private int protocol_state;
    private int protocol_substate;

    private boolean handshake_complete;

    private int bytes_read;
    private int bytes_written;

    private long last_read_time = SystemTime.getCurrentTime();

    private TransportHelperFilter filter;

    private boolean delay_outbound_4;

    private boolean processing_complete;

    private AEMonitor process_mon = new AEMonitor("ProtocolDecoderPHE:process");

    public ProtocolDecoderPHE(TransportHelper _transport, byte[][] _shared_secrets, ByteBuffer _header, ByteBuffer _initial_data,
            ProtocolDecoderAdapter _adapter)

    throws IOException {
        super(false);

        if (!isCryptoOK()) {

            throw (new IOException("PHE crypto broken"));
        }

        transport = _transport;
        // scattering mode for the first KB, that should include the crypto handshake and part of the bittorrent handshake
        transport.setScatteringMode(768 + random.nextInt(256));
        initial_data_out = _initial_data;
        adapter = _adapter;

        if (_shared_secrets == null || _shared_secrets.length == 0) {

            shared_secret = new byte[0];

        } else {

            if (_shared_secrets.length == 1) {

                shared_secret = _shared_secrets[0];

            } else {

                shared_secret = _shared_secrets[random.nextInt(_shared_secrets.length)];
            }

            // System.out.println( "outbound - using crypto secret " + ByteFormatter.encodeString( shared_secret ));
        }

        outbound = _header == null;

        my_supported_protocols = SUPPORTED_PROTOCOLS;

        if (outbound) {

            // if ( !NetworkManager.REQUIRE_CRYPTO_HANDSHAKE ){
            // throw( new IOException( "Crypto encoder selected for outbound but crypto not required" ));
            // }

            // outbound connection, we require a certain minimal level of support

            my_supported_protocols = MIN_CRYPTO;

        } else {

            // incoming. If we require crypto then we use minimum otherwise available

            if (NetworkManager.REQUIRE_CRYPTO_HANDSHAKE) {

                my_supported_protocols = MIN_CRYPTO;
            }
        }

        initCrypto();

        try {
            process_mon.enter();

            transport.registerForReadSelects(new TransportHelper.selectListener() {
                public boolean selectSuccess(TransportHelper helper, Object attachment) {
                    return (ProtocolDecoderPHE.this.selectSuccess(helper, attachment, false));
                }

                public void selectFailure(TransportHelper helper, Object attachment, Throwable msg) {
                    ProtocolDecoderPHE.this.selectFailure(helper, attachment, msg);
                }
            }, null);

            transport.registerForWriteSelects(new TransportHelper.selectListener() {
                public boolean selectSuccess(TransportHelper helper, Object attachment) {
                    return (ProtocolDecoderPHE.this.selectSuccess(helper, attachment, true));
                }

                public void selectFailure(TransportHelper helper, Object attachment, Throwable msg) {
                    ProtocolDecoderPHE.this.selectFailure(helper, attachment, msg);
                }
            }, null);

            transport.pauseWriteSelects();

            if (outbound) {

                protocol_state = PS_OUTBOUND_1;

                transport.pauseReadSelects();

            } else {

                protocol_state = PS_INBOUND_1;

                read_buffer = ByteBuffer.allocate(dh_public_key_bytes.length);

                read_buffer.put(_header);

                bytes_read += _header.limit();
            }
        } finally {

            process_mon.exit();
        }

        process();
    }

    protected void initCrypto()

    throws IOException {
        try {
            KeyPair key_pair = generateDHKeyPair(transport, outbound);

            key_agreement = KeyAgreement.getInstance("DH");

            key_agreement.init(key_pair.getPrivate());

            DHPublicKey dh_public_key = (DHPublicKey) key_pair.getPublic();

            BigInteger dh_y = dh_public_key.getY();

            dh_public_key_bytes = bigIntegerToBytes(dh_y, DH_SIZE_BYTES);

        } catch (Throwable e) {

            throw (new IOException(Debug.getNestedExceptionMessage(e)));
        }
    }

    protected void completeDH(byte[] buffer)

    throws IOException {
        try {
            BigInteger other_dh_y = bytesToBigInteger(buffer, 0, DH_SIZE_BYTES);

            KeyFactory dh_key_factory = KeyFactory.getInstance("DH");

            PublicKey other_public_key = dh_key_factory.generatePublic(new DHPublicKeySpec(other_dh_y, DH_P_BI, DH_G_BI));

            key_agreement.doPhase(other_public_key, true);

            secret_bytes = key_agreement.generateSecret();

            adapter.gotSecret(secret_bytes);

            // System.out.println( "secret = " + ByteFormatter.encodeString( secret_bytes ));

        } catch (Throwable e) {

            throw (new IOException(Debug.getNestedExceptionMessage(e)));
        }
    }

    protected void setupCrypto()

    throws IOException {
        try {
            // "HASH('keyA', S, SKEY)" if you're A
            // "HASH('keyB', S, SKEY)" if you're B

            SHA1Hasher hasher = new SHA1Hasher();

            hasher.update(KEYA_IV);
            hasher.update(secret_bytes);
            hasher.update(shared_secret);

            byte[] a_key = hasher.getDigest();

            hasher = new SHA1Hasher();

            hasher.update(KEYB_IV);
            hasher.update(secret_bytes);
            hasher.update(shared_secret);

            byte[] b_key = hasher.getDigest();

            SecretKeySpec secret_key_spec_a = new SecretKeySpec(a_key, RC4_STREAM_ALG);

            SecretKeySpec secret_key_spec_b = new SecretKeySpec(b_key, RC4_STREAM_ALG);

            write_cipher = new TransportCipher(RC4_STREAM_CIPHER, Cipher.ENCRYPT_MODE, outbound ? secret_key_spec_a : secret_key_spec_b);

            read_cipher = new TransportCipher(RC4_STREAM_CIPHER, Cipher.DECRYPT_MODE, outbound ? secret_key_spec_b : secret_key_spec_a);

        } catch (Throwable e) {

            e.printStackTrace();

            throw (new IOException(Debug.getNestedExceptionMessage(e)));
        }
    }

    /*
     * protected void completeDH( byte[] buffer ) throws IOException { try{ BigInteger other_dh_y = bytesToBigInteger( buffer, 0, DH_SIZE_BYTES );
     * KeyFactory dh_key_factory = KeyFactory.getInstance("DH"); PublicKey other_public_key = dh_key_factory.generatePublic( new DHPublicKeySpec(
     * other_dh_y, DH_P_BI, DH_G_BI )); key_agreement.doPhase( other_public_key, true ); byte[] secret_bytes_64 = key_agreement.generateSecret(); //
     * we only want the first 32 bytes of the secret secret_bytes = new byte[32]; System.arraycopy( secret_bytes_64, 0, secret_bytes, 0, 32 );
     * sha1_secret_bytes = new SHA1Simple().calculateHash( secret_bytes ); SecretKeySpec secret_key_spec_a = new SecretKeySpec( secret_bytes, 0,
     * RC4_STREAM_KEY_SIZE_BYTES, RC4_STREAM_ALG ); SecretKeySpec secret_key_spec_b = new SecretKeySpec( secret_bytes, 16, RC4_STREAM_KEY_SIZE_BYTES,
     * RC4_STREAM_ALG ); write_cipher = new TCPTransportCipher( RC4_STREAM_CIPHER, Cipher.ENCRYPT_MODE, outbound?secret_key_spec_a:secret_key_spec_b
     * ); read_cipher = new TCPTransportCipher( RC4_STREAM_CIPHER, Cipher.DECRYPT_MODE, outbound?secret_key_spec_b:secret_key_spec_a ); }catch(
     * Throwable e ){ throw( new IOException( Debug.getNestedExceptionMessage(e))); } }
     */

    protected void handshakeComplete()

    throws IOException {
        if (selected_protocol == CRYPTO_PLAIN) {

            filter = new TransportHelperFilterTransparent(transport, true);

        } else if (selected_protocol == CRYPTO_XOR) {

            filter = new TransportHelperFilterStreamXOR(transport, secret_bytes);

        } else if (selected_protocol == CRYPTO_RC4) {

            filter = new TransportHelperFilterStreamCipher(transport, read_cipher, write_cipher);

            /*
             * }else if ( selected_protocol == CRYPTO_AES ){ try{ SecretKeySpec secret_key_spec = new SecretKeySpec( secret_bytes, 32,
             * AES_STREAM_KEY_SIZE_BYTES, AES_STREAM_ALG ); AlgorithmParameterSpec spec = new IvParameterSpec( secret_bytes, 48,
             * AES_STREAM_KEY_SIZE_BYTES ); write_cipher = new TCPTransportCipher( AES_STREAM_CIPHER, Cipher.ENCRYPT_MODE, secret_key_spec, spec );
             * read_cipher = new TCPTransportCipher( AES_STREAM_CIPHER, Cipher.DECRYPT_MODE, secret_key_spec, spec ); filter = new
             * TCPTransportHelperFilterStreamCipher( helper, read_cipher, write_cipher ); }catch( Throwable e ){ throw( new IOException(
             * "AES crypto init failed: " + Debug.getNestedExceptionMessage(e))); }
             */

        } else {

            throw (new IOException("Invalid selected protocol '" + selected_protocol + "'"));
        }

        if (initial_data_in != null) {

            filter = new TransportHelperFilterInserter(filter, initial_data_in);
        }

        handshake_complete = true;
    }

    /*
     * X_1 A->B: Diffie Hellman Ya, PadA X_2 B->A: Diffie Hellman Yb, PadB X_3 A->B: HASH('req1', S), HASH('req2', SKEY)^HASH('req3', S), ENCRYPT(VC,
     * crypto_provide, len(PadC), PadC, len(IA)), ENCRYPT(IA) X_4 B->A: ENCRYPT(VC, crypto_select, len(padD), padD ) // , len(IB)), ENCRYPT(IB)
     */

    protected void process()

    throws IOException {
        try {
            process_mon.enter();

            if (handshake_complete) {

                Debug.out("Handshake process already completed");

                return;
            }

            boolean loop = true;

            while (loop) {

                // System.out.println( this + ":" + (outbound?"out: ":"in : ") + protocol_state + "/" + protocol_substate + ": r " + bytes_read +
                // " - " + read_buffer + ", w " + bytes_written + " - " + write_buffer );

                if (protocol_state == PS_OUTBOUND_1) {

                    if (write_buffer == null) {

                        // A sends B Ya + Pa

                        byte[] padding_a = getRandomPadding(getPaddingMax() / 2); // note that /2 also used in calculating max initial packet size
                                                                                  // above

                        write_buffer = ByteBuffer.allocate(dh_public_key_bytes.length + padding_a.length);

                        write_buffer.put(dh_public_key_bytes);

                        write_buffer.put(padding_a);

                        write_buffer.flip();
                    }

                    write(write_buffer);

                    if (!write_buffer.hasRemaining()) {

                        write_buffer = null;

                        protocol_state = PS_INBOUND_2;
                    }

                } else if (protocol_state == PS_INBOUND_1) {

                    // B receives Ya

                    read(read_buffer);

                    if (!read_buffer.hasRemaining()) {

                        read_buffer.flip();

                        byte[] other_dh_public_key_bytes = new byte[read_buffer.remaining()];

                        read_buffer.get(other_dh_public_key_bytes);

                        completeDH(other_dh_public_key_bytes);

                        read_buffer = null;

                        protocol_state = PS_OUTBOUND_2;
                    }

                } else if (protocol_state == PS_OUTBOUND_2) {

                    // B->A: Yb PadB

                    if (write_buffer == null) {

                        byte[] padding_b = getRandomPadding(getPaddingMax() / 2);

                        write_buffer = ByteBuffer.allocate(dh_public_key_bytes.length + padding_b.length);

                        write_buffer.put(dh_public_key_bytes);

                        write_buffer.put(padding_b);

                        write_buffer.flip();
                    }

                    write(write_buffer);

                    if (!write_buffer.hasRemaining()) {

                        write_buffer = null;

                        protocol_state = PS_INBOUND_3;
                    }

                } else if (protocol_state == PS_INBOUND_2) {

                    // A receives: Yb

                    if (read_buffer == null) {

                        read_buffer = ByteBuffer.allocate(dh_public_key_bytes.length);
                    }

                    read(read_buffer);

                    if (!read_buffer.hasRemaining()) {

                        read_buffer.flip();

                        byte[] other_dh_public_key_bytes = new byte[read_buffer.remaining()];

                        read_buffer.get(other_dh_public_key_bytes);

                        completeDH(other_dh_public_key_bytes);

                        // A initiates SKEY so we can now set up crypto

                        setupCrypto();

                        read_buffer = null;

                        protocol_state = PS_OUTBOUND_3;
                    }

                } else if (protocol_state == PS_OUTBOUND_3) {

                    // A->B: HASH('req1', S), HASH('req2', SKEY)^HASH('req3', S), ENCRYPT(VC, crypto_provide, len(PadC), PadC, len(IA)), ENCRYPT(IA)

                    if (write_buffer == null) {

                        int initial_data_out_len = initial_data_out == null ? 0 : initial_data_out.remaining();

                        // padding_a here is half of the padding from before

                        int pad_max = getPaddingMax();

                        byte[] padding_a = getRandomPadding(pad_max / 2);

                        byte[] padding_c = getZeroPadding(pad_max);

                        write_buffer =
                                ByteBuffer.allocate(padding_a.length + 20 + 20 + (VC.length + 4 + 2 + padding_c.length + 2) + initial_data_out_len);

                        write_buffer.put(padding_a);

                        // HASH('req1', S)

                        SHA1Hasher hasher = new SHA1Hasher();

                        hasher.update(REQ1_IV);
                        hasher.update(secret_bytes);

                        byte[] sha1 = hasher.getDigest();

                        write_buffer.put(sha1);

                        // HASH('req2', SKEY)^HASH('req3', S)

                        hasher = new SHA1Hasher();

                        hasher.update(REQ2_IV);
                        hasher.update(shared_secret);

                        byte[] sha1_1 = hasher.getDigest();

                        hasher = new SHA1Hasher();

                        hasher.update(REQ3_IV);
                        hasher.update(secret_bytes);

                        byte[] sha1_2 = hasher.getDigest();

                        for (int i = 0; i < sha1_1.length; i++) {

                            sha1_1[i] ^= sha1_2[i];
                        }

                        write_buffer.put(sha1_1);

                        // ENCRYPT(VC, crypto_provide, len(PadC), PadC, len(IA)

                        write_buffer.put(write_cipher.update(VC));

                        write_buffer.put(write_cipher.update(new byte[] { 0, 0, 0, my_supported_protocols }));

                        write_buffer.put(write_cipher.update(new byte[] { (byte) (padding_c.length >> 8), (byte) padding_c.length }));

                        write_buffer.put(write_cipher.update(padding_c));

                        write_buffer.put(write_cipher.update(new byte[] { (byte) (initial_data_out_len >> 8), (byte) initial_data_out_len }));

                        if (initial_data_out_len > 0) {

                            int save_pos = initial_data_out.position();

                            write_cipher.update(initial_data_out, write_buffer);

                            // reset in case buffer needs to be used again by caller

                            initial_data_out.position(save_pos);

                            initial_data_out = null;
                        }

                        write_buffer.flip();
                    }

                    write(write_buffer);

                    if (!write_buffer.hasRemaining()) {

                        write_buffer = null;

                        protocol_state = PS_INBOUND_4;
                    }

                } else if (protocol_state == PS_INBOUND_3) {

                    // B receives: HASH('req1', S), HASH('req2', SKEY)^HASH('req3', S), ENCRYPT(VC, crypto_provide, len(PadC), PadC, len(IA)),
                    // ENCRYPT(IA)

                    if (read_buffer == null) {

                        read_buffer = ByteBuffer.allocate(20 + PADDING_MAX);

                        read_buffer.limit(20);

                        SHA1Hasher hasher = new SHA1Hasher();

                        hasher.update(REQ1_IV);
                        hasher.update(secret_bytes);

                        padding_skip_marker = hasher.getDigest();

                        protocol_substate = 1;
                    }

                    while (true) {

                        read(read_buffer);

                        if (read_buffer.hasRemaining()) {

                            break;
                        }

                        if (protocol_substate == 1) {

                            // skip up to HASH('req1', S)

                            int limit = read_buffer.limit();

                            read_buffer.position(limit - 20);

                            boolean match = true;

                            for (int i = 0; i < 20; i++) {

                                if (read_buffer.get() != padding_skip_marker[i]) {

                                    match = false;

                                    break;
                                }
                            }

                            if (match) {

                                read_buffer = ByteBuffer.allocate(20 + VC.length + 4 + 2);

                                protocol_substate = 2;

                                break;

                            } else {

                                if (limit == read_buffer.capacity()) {

                                    throw (new IOException("PHE skip to SHA1 marker failed"));
                                }

                                read_buffer.limit(limit + 1);

                                read_buffer.position(limit);
                            }
                        } else if (protocol_substate == 2) {

                            // find SKEY using HASH('req2', SKEY)^HASH('req3', S) , ENCRYPT(VC, crypto_provide, len(PadC),

                            read_buffer.flip();

                            final byte[] decode = new byte[20];

                            read_buffer.get(decode);

                            SHA1Hasher hasher = new SHA1Hasher();

                            hasher.update(REQ3_IV);
                            hasher.update(secret_bytes);

                            byte[] sha1 = hasher.getDigest();

                            for (int i = 0; i < decode.length; i++) {

                                decode[i] ^= sha1[i];
                            }

                            synchronized (global_shared_secrets) {

                                shared_secret = (byte[]) global_shared_secrets.get(new HashWrapper(decode));
                            }

                            if (shared_secret == null) {

                                throw (new IOException("No matching shared secret"));
                            }

                            // System.out.println( "inbound - using crypto secret " + ByteFormatter.encodeString( shared_secret ));

                            setupCrypto();

                            byte[] crypted = new byte[VC.length + 4 + 2];

                            read_buffer.get(crypted);

                            byte[] plain = read_cipher.update(crypted);

                            byte other_supported_protocols = plain[VC.length + 3];

                            int common_protocols = my_supported_protocols & other_supported_protocols;

                            if ((common_protocols & CRYPTO_PLAIN) != 0) {

                                selected_protocol = CRYPTO_PLAIN;

                            } else if ((common_protocols & CRYPTO_XOR) != 0) {

                                selected_protocol = CRYPTO_XOR;

                            } else if ((common_protocols & CRYPTO_RC4) != 0) {

                                selected_protocol = CRYPTO_RC4;

                            } else if ((common_protocols & CRYPTO_AES) != 0) {

                                selected_protocol = CRYPTO_AES;

                            } else {

                                throw (new IOException("No crypto protocol in common: mine = " + Integer.toHexString((byte) my_supported_protocols)
                                        + ", theirs = " + Integer.toHexString((byte) other_supported_protocols)));

                            }

                            int padding = ((plain[VC.length + 4] & 0xff) << 8) + (plain[VC.length + 5] & 0xff);

                            if (padding > PADDING_MAX) {

                                throw (new IOException("Invalid padding '" + padding + "'"));
                            }

                            read_buffer = ByteBuffer.allocate(padding + 2);

                            // skip the padding

                            protocol_substate = 3;

                        } else if (protocol_substate == 3) {

                            // ENCRYPT( len(IA)), { ENCRYPT(IA) }

                            read_buffer.flip();

                            byte[] data = new byte[read_buffer.remaining()];

                            read_buffer.get(data);

                            data = read_cipher.update(data);

                            int ia_len = 0xffff & (((data[data.length - 2] & 0xff) << 8) + (data[data.length - 1] & 0xff));

                            if (ia_len > 65535) {

                                throw (new IOException("Invalid IA length '" + ia_len + "'"));
                            }

                            if (ia_len > 0) {

                                read_buffer = ByteBuffer.allocate(ia_len);

                                // skip the padding

                                protocol_substate = 4;

                            } else {

                                read_buffer = null;

                                protocol_state = PS_OUTBOUND_4;

                                break;
                            }
                        } else if (protocol_substate == 4) {

                            // ENCRYPT(IA)

                            read_buffer.flip();

                            byte[] data = new byte[read_buffer.remaining()];

                            read_buffer.get(data);

                            data = read_cipher.update(data);

                            // hack alert - we can delay the writing of the outbound_4 packet if this is an incoming packet with
                            // a piggybacked bt handshake as we know that we'll be sending our own handshake back out pretty soon
                            // and it'll take the delayed data with it. To be more generic we'd need to add a callback to the pattern
                            // matcher to allow it to decide whether delaying was sensible / or stick a timer on the delayed data

                            delay_outbound_4 = new String(data).indexOf("BitTorrent") != -1;

                            // System.out.println( "Initial Data In: " + new String( data ) + "->delay=" +delay_outbound_4 );

                            initial_data_in = ByteBuffer.wrap(data);

                            read_buffer = null;

                            protocol_state = PS_OUTBOUND_4;

                            break;
                        }
                    }
                } else if (protocol_state == PS_OUTBOUND_4) {

                    // B->A: ENCRYPT(VC, crypto_select, len(padD), padD, // len(IB)), ENCRYPT(IB)

                    if (write_buffer == null) {

                        int pad_max = getPaddingMax();

                        byte[] padding_b = getRandomPadding(pad_max / 2); // half padding b sent here

                        byte[] padding_d = getZeroPadding(pad_max);

                        write_buffer = ByteBuffer.allocate(padding_b.length + VC.length + 4 + 2 + padding_d.length); // + 2 + initial_data_out.length
                                                                                                                     // );

                        write_buffer.put(padding_b);

                        write_buffer.put(write_cipher.update(VC));

                        write_buffer.put(write_cipher.update(new byte[] { 0, 0, 0, selected_protocol }));

                        write_buffer.put(write_cipher.update(new byte[] { (byte) (padding_d.length >> 8), (byte) padding_d.length }));

                        write_buffer.put(write_cipher.update(padding_d));

                        // write_buffer.put( write_cipher.update( new byte[]{ (byte)(initial_data_out.length>>8),(byte)initial_data_out.length }));

                        // write_buffer.put( write_cipher.update( initial_data_out ));

                        write_buffer.flip();
                    }

                    if (delay_outbound_4) {

                        if (transport.delayWrite(write_buffer)) {

                            write_buffer = null;

                            handshakeComplete();

                        } else {

                            delay_outbound_4 = false;
                        }
                    }

                    if (!delay_outbound_4) {

                        write(write_buffer);

                        if (!write_buffer.hasRemaining()) {

                            write_buffer = null;

                            handshakeComplete();
                        }
                    }
                } else if (protocol_state == PS_INBOUND_4) {

                    // B->A: ENCRYPT(VC, crypto_select, len(padD), padD // , len(IB)), ENCRYPT(IB)

                    if (read_buffer == null) {

                        read_buffer = ByteBuffer.allocate(VC.length + PADDING_MAX);

                        read_buffer.limit(VC.length);

                        padding_skip_marker = new byte[VC.length];

                        padding_skip_marker = read_cipher.update(padding_skip_marker);

                        protocol_substate = 1;
                    }

                    while (true) {

                        read(read_buffer);

                        if (read_buffer.hasRemaining()) {

                            break;
                        }

                        if (protocol_substate == 1) {

                            // skip up to marker

                            int limit = read_buffer.limit();

                            read_buffer.position(limit - VC.length);

                            boolean match = true;

                            for (int i = 0; i < VC.length; i++) {

                                if (read_buffer.get() != padding_skip_marker[i]) {

                                    match = false;

                                    break;
                                }
                            }

                            if (match) {

                                read_buffer = ByteBuffer.allocate(4 + 2);

                                protocol_substate = 2;

                                break;

                            } else {

                                if (limit == read_buffer.capacity()) {

                                    throw (new IOException("PHE skip to SHA1 marker failed"));
                                }

                                read_buffer.limit(limit + 1);

                                read_buffer.position(limit);
                            }
                        } else if (protocol_substate == 2) {

                            // ENCRYPT( crypto_select, len(padD))

                            read_buffer.flip();

                            byte[] crypted = new byte[4 + 2];

                            read_buffer.get(crypted);

                            byte[] plain = read_cipher.update(crypted);

                            selected_protocol = plain[3];

                            if ((selected_protocol & my_supported_protocols) == 0) {

                                throw (new IOException("Selected protocol has nothing in common: mine = "
                                        + Integer.toHexString((byte) my_supported_protocols) + ", theirs = "
                                        + Integer.toHexString((byte) selected_protocol)));

                            }

                            int pad_len = 0xffff & (((plain[4] & 0xff) << 8) + (plain[5] & 0xff));

                            if (pad_len > 65535) {

                                throw (new IOException("Invalid pad length '" + pad_len + "'"));
                            }

                            read_buffer = ByteBuffer.allocate(pad_len); // + 2 );

                            protocol_substate = 3;

                        } else if (protocol_substate == 3) {

                            read_buffer.flip();

                            byte[] data = new byte[read_buffer.remaining()];

                            read_buffer.get(data);

                            data = read_cipher.update(data);

                            handshakeComplete();

                            read_buffer = null;

                            break;
                            /*
                             * int ib_len = 0xffff & ((( data[data.length-2] & 0xff ) << 8 ) + ( data[data.length-1] & 0xff )); if ( ib_len > 65535 ){
                             * throw( new IOException( "Invalid IB length '" + ib_len + "'" )); } read_buffer = ByteBuffer.allocate( ib_len );
                             * protocol_substate = 4; }else{ read_buffer.flip(); byte[] data = new byte[read_buffer.remaining()]; read_buffer.get(
                             * data ); initial_data_in = read_cipher.update( data ); handshakeComplete(); read_buffer = null; break;
                             */
                        }
                    }
                }

                if (handshake_complete) {

                    transport.cancelReadSelects();

                    transport.cancelWriteSelects();

                    loop = false;

                    complete();

                } else {

                    if (read_buffer == null) {

                        transport.pauseReadSelects();

                    } else {

                        transport.resumeReadSelects();

                        loop = false;

                    }

                    if (write_buffer == null) {

                        transport.pauseWriteSelects();

                    } else {

                        transport.resumeWriteSelects();

                        loop = false;
                    }
                }
            }
        } catch (Throwable e) {

            failed(e);

            if (e instanceof IOException) {

                throw ((IOException) e);

            } else {

                throw (new IOException(Debug.getNestedExceptionMessage(e)));
            }
        } finally {

            process_mon.exit();
        }
    }

    /*
     * *** OUTBOUND_1 A sends B odd/even byte + Ya + Pa*** INBOUND_1 B receives Ya B computes Yb B computes S and HS*** OUTBOUND_2 B sends A Yb + HS(
     * "supported methods" + len(Pb)) + Pb*** INBOUND_2 A receives Yb A computes S and HS A receives HS( "supported methods" + len(Pb)) and decrypts
     * using HS A skips len(Pb) random bytes*** OUTBOUND_3 A sends SHA1(S) + HS( "selected method" + len(Pc)) + Pc + selectedCrypt( payload )***
     * INBOUND_3 B skips Pa bytes until receives SHA1(S) B decrypts "selected method" + len(Pc) and skips len(Pc) bytes to get to selectedCrypt(
     * payload... ) B sends A selectedCrypt( payload... )
     */

    /*
     * protected void process() throws IOException { try{ process_mon.enter(); if ( handshake_complete ){ Debug.out(
     * "Handshake process already completed" ); return; } boolean loop = true; while( loop ){ if ( protocol_state == PS_OUTBOUND_1 ){ if (
     * write_buffer == null ){ // A sends B odd/even Ya + Pa byte[] padding = getPadding(); write_buffer = ByteBuffer.allocate(
     * dh_public_key_bytes.length + padding.length ); write_buffer.put( dh_public_key_bytes ); write_buffer.put( padding ); write_buffer.flip(); }
     * write( write_buffer ); if ( !write_buffer.hasRemaining()){ write_buffer = null; protocol_state = PS_INBOUND_2; } }else if ( protocol_state ==
     * PS_OUTBOUND_2 ){ // B sends A Yb + HS( "supported methods" + len(Pb)) + Pb if ( write_buffer == null ){ byte[] padding = getPadding();
     * write_buffer = ByteBuffer.allocate( dh_public_key_bytes.length + 4 + 2 + padding.length ); write_buffer.put( dh_public_key_bytes ); // 4 bytes
     * for my supported protocols write_buffer.put( write_cipher.update( new byte[]{ 0, 0, 0, my_supported_protocols })); write_buffer.put(
     * write_cipher.update( new byte[]{ (byte)(padding.length>>8),(byte)padding.length })); write_buffer.put( padding ); write_buffer.flip(); } write(
     * write_buffer ); if ( !write_buffer.hasRemaining()){ write_buffer = null; protocol_state = PS_INBOUND_3; } }else if ( protocol_state ==
     * PS_OUTBOUND_3 ){ // A sends SHA1(S) + HS( "selected method" + len(Pc)) + Pc + selectedCrypt( payload ) if ( write_buffer == null ){ byte[]
     * padding = getPadding(); write_buffer = ByteBuffer.allocate( 20 + 4 + 2 + padding.length ); write_buffer.put( sha1_secret_bytes );
     * write_buffer.put( write_cipher.update( new byte[]{ 0, 0, 0, selected_protocol })); write_buffer.put( write_cipher.update( new byte[]{
     * (byte)(padding.length>>8),(byte)padding.length })); write_buffer.put( padding ); write_buffer.flip(); } write( write_buffer ); if (
     * !write_buffer.hasRemaining()){ write_buffer = null; handshakeComplete(); } }else if ( protocol_state == PS_INBOUND_1 ){ // B receives marker +
     * Ya read( read_buffer ); if ( !read_buffer.hasRemaining()){ read_buffer.flip(); byte[] other_dh_public_key_bytes = new
     * byte[read_buffer.remaining()]; read_buffer.get( other_dh_public_key_bytes ); completeDH( other_dh_public_key_bytes ); read_buffer = null;
     * protocol_state = PS_OUTBOUND_2; } }else if ( protocol_state == PS_INBOUND_2 ){ //A receives Yb //A computes S and HS //A receives HS(
     * "supported methods" + len(Pb)) and decrypts using HS //A skips len(Pb) random bytes if ( read_buffer == null ){ read_buffer =
     * ByteBuffer.allocate( dh_public_key_bytes.length + 6 ); protocol_substate = 1; } while( true ){ read( read_buffer ); if (
     * read_buffer.hasRemaining()){ break; } if ( protocol_substate == 1 ){ read_buffer.flip(); byte[] other_dh_public_key_bytes_etc =
     * read_buffer.array(); completeDH( other_dh_public_key_bytes_etc ); byte[] etc = read_cipher.update( other_dh_public_key_bytes_etc,
     * DH_SIZE_BYTES, 6 ); byte other_supported_protocols = etc[3]; int common_protocols = my_supported_protocols & other_supported_protocols; if ((
     * common_protocols & CRYPTO_PLAIN )!= 0 ){ selected_protocol = CRYPTO_PLAIN; }else if (( common_protocols & CRYPTO_XOR )!= 0 ){ selected_protocol
     * = CRYPTO_XOR; }else if (( common_protocols & CRYPTO_RC4 )!= 0 ){ selected_protocol = CRYPTO_RC4; }else if (( common_protocols & CRYPTO_AES )!=
     * 0 ){ selected_protocol = CRYPTO_AES; }else{ throw( new IOException( "No crypto protocol in common: mine = " +
     * Integer.toHexString((byte)my_supported_protocols) + ", theirs = " + Integer.toHexString((byte)other_supported_protocols))); } int padding = ((
     * etc[4] & 0xff ) << 8 ) + ( etc[5] & 0xff ); if ( padding > PADDING_MAX ){ throw( new IOException( "Invalid padding '" + padding + "'" )); }
     * read_buffer = ByteBuffer.allocate( padding ); protocol_substate = 2; }else{ read_buffer = null; protocol_state = PS_OUTBOUND_3; break; } }
     * }else if ( protocol_state == PS_INBOUND_3 ){ // B skips Pa bytes until receives SHA1(S) // B decrypts "selected method" + len(Pc) and skips
     * len(Pc) bytes if ( read_buffer == null ){ read_buffer = ByteBuffer.allocate( 20 + PADDING_MAX ); read_buffer.limit( 20 ); protocol_substate =
     * 1; } while( true ){ read( read_buffer ); if ( read_buffer.hasRemaining()){ break; } if ( protocol_substate == 1 ){ int limit =
     * read_buffer.limit(); read_buffer.position( limit - 20 ); boolean match = true; for (int i=0;i<20;i++){ if ( read_buffer.get() !=
     * sha1_secret_bytes[i] ){ match = false; break; } } if ( match ){ read_buffer = ByteBuffer.allocate( 6 ); protocol_substate = 2; break; }else{ if
     * ( limit == read_buffer.capacity()){ throw( new IOException( "PHE skip to SHA1 marker failed" )); } read_buffer.limit( limit + 1 );
     * read_buffer.position( limit ); } }else if ( protocol_substate == 2 ){ read_buffer.flip(); byte[] etc = read_cipher.update(
     * read_buffer.array()); selected_protocol = etc[3]; int padding = (( etc[4] & 0xff ) << 8 ) + ( etc[5] & 0xff ); if ( padding > PADDING_MAX ){
     * throw( new IOException( "Invalid padding '" + padding + "'" )); } read_buffer = ByteBuffer.allocate( padding ); protocol_substate = 3; }else{
     * read_buffer = null; handshakeComplete(); break; } } } if ( handshake_complete ){ read_selector.cancel( channel ); write_selector.cancel(
     * channel ); loop = false; complete(); }else{ if ( read_buffer == null ){ read_selector.pauseSelects( channel ); }else{
     * read_selector.resumeSelects ( channel ); loop = false; } if ( write_buffer == null ){ write_selector.pauseSelects( channel ); }else{
     * write_selector.resumeSelects ( channel ); loop = false; } } } }catch( Throwable e ){ failed( e ); if ( e instanceof IOException ){
     * throw((IOException)e); }else{ throw( new IOException( Debug.getNestedExceptionMessage(e))); } }finally{ process_mon.exit(); } }
     */

    protected void read(ByteBuffer buffer)

    throws IOException {
        int len = transport.read(buffer);

        // System.out.println( "read:" + this + "/" + protocol_state + "/" + protocol_substate + " -> " + len +"[" + buffer +"]");

        if (len < 0) {

            throw (new IOException("end of stream on socket read - phe: " + getString()));
        }

        bytes_read += len;
    }

    protected void write(ByteBuffer buffer)

    throws IOException {
        // System.out.println( "write pre:" + this + "/" + protocol_state + "/" + protocol_substate + " - " + buffer );

        int len = transport.write(buffer, false);

        // System.out.println( "write:" + this + "/" + protocol_state + "/" + protocol_substate + " -> " + len +"[" + buffer +"]");

        if (len < 0) {

            throw (new IOException("bytes written < 0 "));
        }

        bytes_written += len;
    }

    public boolean selectSuccess(TransportHelper transport, Object attachment, boolean write_operation) {
        try {
            int old_bytes_read = bytes_read;
            int old_bytes_written = bytes_written;

            process();

            if (write_operation) {

                return (bytes_written != old_bytes_written);

            } else {

                boolean progress = bytes_read != old_bytes_read;

                if (progress) {

                    last_read_time = SystemTime.getCurrentTime();
                }

                return (progress);
            }

        } catch (Throwable e) {

            failed(e);

            return (false);
        }
    }

    public void selectFailure(TransportHelper transport, Object attachment, Throwable msg) {
        failed(msg);
    }

    protected byte[] bigIntegerToBytes(BigInteger bi, int num_bytes) {
        String str = bi.toString(16);

        while (str.length() < num_bytes * 2) {
            str = "0" + str;
        }

        return (ByteFormatter.decodeString(str));
    }

    protected BigInteger bytesToBigInteger(byte[] bytes, int offset, int len) {
        return (new BigInteger(ByteFormatter.encodeString(bytes, offset, len), 16));
    }

    protected int getPaddingMax() {
        if (transport.minimiseOverheads()) {

            return (PADDING_MAX_LIMITED);

        } else {

            return (PADDING_MAX_NORMAL);
        }
    }

    protected static synchronized byte[] getRandomPadding(int max_len) {
        byte[] bytes = new byte[random.nextInt(max_len)];

        random.nextBytes(bytes);

        return (bytes);
    }

    protected static synchronized byte[] getZeroPadding(int max_len) {
        byte[] bytes = new byte[random.nextInt(max_len)];

        return (bytes);
    }

    protected static KeyPair generateDHKeyPair(TransportHelper transport, boolean outbound)

    throws IOException {
        if (dh_key_generator == null) {

            throw (new IOException("Crypto not setup"));
        }

        synchronized (dh_key_generator) {

            if (!outbound) {

                InetSocketAddress is_address = transport.getAddress();

                byte[] address = AddressUtils.getAddressBytes(is_address);

                int hit_count = generate_bloom.add(address);

                long now = SystemTime.getCurrentTime();

                // allow up to 10% bloom filter utilisation

                if (generate_bloom.getSize() / generate_bloom.getEntryCount() < 10) {

                    generate_bloom = BloomFilterFactory.createAddRemove4Bit(generate_bloom.getSize() + BLOOM_INCREASE);

                    generate_bloom_create_time = now;

                    Logger.log(new LogEvent(LOGID, "PHE bloom: size increased to " + generate_bloom.getSize()));

                } else if (now < generate_bloom_create_time || now - generate_bloom_create_time > BLOOM_RECREATE) {

                    generate_bloom = BloomFilterFactory.createAddRemove4Bit(generate_bloom.getSize());

                    generate_bloom_create_time = now;
                }

                if (hit_count >= 15) {

                    Logger.log(new LogEvent(LOGID, "PHE bloom: too many recent connection attempts from " + transport.getAddress()));

                    throw (new IOException("Too many recent connection attempts (phe)"));
                }

                long since_last = now - last_dh_incoming_key_generate;

                long delay = 100 - since_last;

                // limit key gen operations to 10 a second

                if (delay > 0 && delay < 100) {

                    try {
                        Thread.sleep(delay);

                    } catch (Throwable e) {
                    }
                }

                last_dh_incoming_key_generate = now;
            }

            KeyPair res = dh_key_generator.generateKeyPair();

            return (res);
        }
    }

    protected void complete() {
        // System.out.println( (outbound?"out: ":"in :") + " complete, r " + bytes_read + ", w " + bytes_written + ", initial data = " +
        // initial_data_in.length + "/" + initial_data_out.length );

        processing_complete = true;

        transport.setScatteringMode(0);

        adapter.decodeComplete(this, initial_data_out);
    }

    protected void failed(Throwable cause) {
        // System.out.println( (outbound?"out: ":"in :") + " failed, " + cause.getMessage());

        processing_complete = true;

        transport.cancelReadSelects();

        transport.cancelWriteSelects();

        adapter.decodeFailed(this, cause);
    }

    public boolean isComplete(long now) {
        return (processing_complete);
    }

    public TransportHelperFilter getFilter() {
        return (filter);
    }

    public long getLastReadTime() {
        long now = SystemTime.getCurrentTime();

        if (last_read_time > now) {

            last_read_time = now;
        }

        return (last_read_time);
    }

    public String getString() {
        return ("state=" + protocol_state + ",sub=" + protocol_substate + ",in=" + bytes_read + ",out=" + bytes_written);
    }
}
